﻿using System;
using System.Diagnostics;
using System.Xml;
using System.Xml.XPath;
using System.Text;
using System.Configuration;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Microsoft.BizTalk.Samples.EDIGuidance.HelperComponents
{
    [Serializable]
    public class XmlHelper
    {
        /// <summary>
        /// Checks whether the document is not null. Else, it throws an exception.
        /// </summary>
        /// <param name="contextNode"></param>
        public static void CheckDocument(XmlDocument doc)
        {
            if (null == doc)
            {
                throw new ArgumentNullException("XmlDocument object is null.");
            }
        }

        /// <summary>
        /// Add an attribute with a specified name and value into the node selected by the XPath
        /// </summary>
        /// <param name="contextNode">RuleEngine wrapper for an XmlDocument (or some part of it)</param>
        /// <param name="xpath">XPath that must select a node (null or "" to use current node)</param>
        /// <param name="attributeName">Attribute name</param>
        /// <param name="attributeValue">Attribute value</param>
        public static void AddAttribute(XmlNode doc, string xPath, string attributeName, string attributeValue)
        {
            // can we find the XPath indicated?
            // we need an XmlElement in order to set the attribute, not the usual XmlNode
            // if the result is not an XmlElement, we don't work
            XmlElement node = LocateNode(doc, xPath) as XmlElement;

            // if we found a matching node, add the attribute
            if (null != node)
            {
                node.SetAttribute(attributeName, attributeValue);
            }
        }

        /// <summary>
        /// Adds a new child node into the node selected by <paramref name="xpathToNode"/>provided that it doesn't already exist. 
        /// If a node does exist, then nothing is added and the presently-existing node is returned.
        /// </summary>
        /// <param name="selectorNode">the XmlNode to which we will be adding any child nodes in <paramref name="xpathToNode"/>.</param>
        /// <param name="xpathToNode">XPath that must select a node (null or "" to use current node)</param>
        /// <returns>either the XmlNode added or the node that already exists or null if unable to add the node.</returns>
        public static XmlNode AddNodeIfNotExists(XmlNode selectorNode, string xpathToNode)
        {
            // XmlNode doesn't have a XmlNamespaceManager associated with it, 
            // only XmlDocument. So, if we've received an XmlNode portion, then we
            // need to get the node's owner. Otherwise, we'll get a null reference exception
            // when we call CreateNamespaceManager().
            XmlDocument doc = (selectorNode.NodeType != XmlNodeType.Document) ? selectorNode.OwnerDocument : selectorNode as XmlDocument;

            // Call the recursive routine.
            return AddNodeIfNotExists(selectorNode, CreateNamespaceManager(doc), xpathToNode);
        }

        /// <summary>
        /// Adds a new child node into the node selected by <paramref name="xpathToNode"/>provided that it doesn't already exist. 
        /// If a node does exist, then nothing is added and the presently-existing node is returned.
        /// </summary>
        /// <param name="selectorNode">the XmlNode to which we will be adding any child nodes in <paramref name="xpathToNode"/>.</param>
        /// <param name="xpathToNode">XPath that must select a node (null or "" to use current node)</param>
        /// <returns>either the XmlNode added or the node that already exists or null if unable to add the node.</returns>
        private static XmlNode AddNodeIfNotExists(XmlNode contextNode, XmlNamespaceManager nsManager, string xpathToNode)
        {
            // Call the recursive routine.
            return AddNodeIfNotExists(contextNode, nsManager, xpathToNode, string.Empty, null);
        }

        /// <summary>
        /// Adds a new child node into the node selected by <paramref name="xpathToNode"/>provided that it doesn't already exist. 
        /// If a node does exist, then nothing is added and the presently-existing node is returned.
        /// </summary>
        /// <param name="selectorNode">the XmlNode to which we will be adding any child nodes in <paramref name="xpathToNode"/>.</param>
        /// <param name="xpathToNode">XPath that must select a node (null or "" to use current node)</param>
        /// <param name="innerxml">to set the InnerXml property of the added node.</param>
        /// <returns>either the XmlNode added or the node that already exists or null if unable to add the node.</returns>
        private static XmlNode AddNodeIfNotExists(XmlNode contextNode, XmlNamespaceManager nsManager, string xpathToNode, string innerxml, XPathHelper h)
        {
            //todo put recursion elsewhere. Refer to overloaded routines.

            XmlNode newNode = null;

            // Try to find an existing node with the Xpath given.
            XmlNode node = LocateNode(contextNode, xpathToNode);

            // If we found a node in the document...
            if (null != node)
            {
                // Only set the inner xml to what we've been building every time we recursed.
                if (!string.IsNullOrEmpty(innerxml))
                {
                    node.InnerXml += innerxml;
                    newNode = node;
                }
                else
                {
                    // There already is a node - with the given Xpath - in the document, so don't add.
                    // Just go ahead and return the node we found, anyways.
                    newNode = node;
                }
            }
            else
            {
                // Couldn't find a node, so keep parsing through the xpath until we get a hit.
                if (h == null)
                {
                    // CAVEAT EMPTOR: I am assuming that when this method is first called, 
                    // the XPathHelper object argument instance - h - is null.
                    h = new XPathHelper(xpathToNode);

                    // Get it all up and running.
                    h.Bootstrap();
                }

                // Nest the current node name into the innerxml that we have so far.
                innerxml = string.Format("<{0}>{1}</{0}>", h.CurrentNodeName, innerxml);

                // Traverse up to the parent in the xpathToNode.
                h.GotoParent();

                // Recurse.
                newNode = AddNodeIfNotExists(contextNode, nsManager, h.XpathToCurrentNode, innerxml, h);
            }
            return newNode;
        }

        /// <summary>
        /// Add an new node with a specified name into the node selected by the XPath
        /// </summary>
        /// <param name="contextNode">document to add the node</param>
        /// <param name="xPathToParent">XPath that must select the parent node of the new node you wish to add</param>
        /// <param name="localName">Name for the new node</param>
        /// <returns>the added node or null if unable to add</returns>
        public static XmlNode AddNode(XmlNode doc, string xPathToParent, string localName)
        {
            #region "Predicates"
            AddNodeIfNotExists(doc, xPathToParent);
            #endregion

            // can we find the parentNode with the given XPath?
            XmlNode parentNode = doc.SelectSingleNode(xPathToParent);

            if (null == parentNode)
            {
                return null;
            }

            // Determine the ownerDocument parentNode for the document.
            // If the XPath selects the TXD, it will not have a ownerDocument if it is an XmlDocument.
            // In that case, simply use the document from the TXD
            XmlDocument ownerDocument = parentNode.OwnerDocument;

            if (null == ownerDocument)
            {
                return null;
            }

            // Create a new parentNode and add it in.
            XmlElement newNode = ownerDocument.CreateElement(localName);
            parentNode.AppendChild(newNode);

            return newNode;
        }

        /// <summary>
        /// Add an new node with a specified name into the node selected by the XPath
        /// </summary>
        /// <param name="contextNode">document to add the node</param>
        /// <param name="xPathToParent">XPath that must select the parent node of the new node you wish to add</param>
        /// <param name="localName">Name for the new node</param>
        /// <returns>the added node or null if unable to add</returns>
        public static XmlNode AddNode(XmlNode contextNode, string xPathToParent, string localName, string innerText)
        {
            #region "Predicates"
            // add parent node if it doesn't exist
            AddNodeIfNotExists(contextNode, xPathToParent);
            #endregion

            // add node
            XmlNode addedNode = AddNode(contextNode, xPathToParent, localName);

            // give it a value
            if (null != addedNode) { addedNode.InnerText = innerText; }

            return addedNode;
        }

        /// <summary>
        /// Adds a node to every parent node found with <paramref name="selectorXpath"/> and sets the value 
        /// of the added node.
        /// </summary>
        /// <param name="contextNode">the node whose context <paramref name="selectorXpath"/> is based on</param>
        /// <param name="selectorXpath">from <paramref name="contextNode"/>, this gets all 
        /// the nodes to which you want to add children node(s)</param>
        /// <param name="newNodeXpath">xpath to the node wanting to add to each node found with <paramref name="selectorXpath"/>
        /// IMPORTANT:  This has to be in "proper" format i.e. /*[local-name()='foo' and namespace-uri()='...']/*[...] 
        /// in order for the method to work properly.</param>
        /// <param name="value">the value of the added node to which to set</param>
        public static void AddToAllNodes(XmlNode contextNode, string selectorXpath, string newNodeXpath, string value)
        {
            XmlNode newNode = null;

            foreach (XmlNode selectorNode in contextNode.SelectNodes(selectorXpath))
            {
                // Add '.' to query starting from the xpath node, if not already there.
                // TODO GET RID OF THIS, BUT UPDATE THE CALLING POLICIES.
                if (newNodeXpath[0] != '.') { newNodeXpath = newNodeXpath.Insert(0, "."); }

                AddNodeIfNotExists(selectorNode, newNodeXpath);

                // If you find the node you just added (If you didn't, something's wrong!),
                // set the InnerText property to the value passed in.
                if (null != (newNode = selectorNode.SelectSingleNode(newNodeXpath))) { newNode.InnerText = value; }
                else
                {
                    //couldn't find the node we just added...unable to set the InnerText property
                }
            }
        }

        /// <summary>
        /// For each of the nodes found in <paramref name="contextNode"/> with <paramref name="selectorXpath"/>,
        /// add all of the XML implied from <paramref name="xPathToParent"/>, including every axis in between. 
        /// To parent, add an element with name <paramref name="localName"/> and set this element's InnerXml
        /// to <paramref name="innerXml"/>
        /// </summary>
        /// <param name="doc">TypedXmlDocument object instance</param>
        /// <param name="selectorXpath">XPath that will select the nodes to iterate over.</param>
        /// <param name="xPathToParent">XPath to parent of node to be added within each node selected by <paramref name="selector"/>
        /// This has to be in "proper" format i.e. /*[local-name()='foo' and namespace-uri()='...']/*[...] 
        /// in order for the method to work properly.</param>
        /// <param name="localName">localName of the node to be added</param>
        /// <param name="innerXml">InnerXml of the node to be added</param>
        public static void AddToAllNodesWithXml(XmlNode contextNode, string selectorXpath, string xPathToParent, string localName, string innerXml)
        {

            foreach (XmlNode selectorNode in contextNode.SelectNodes(selectorXpath))
            {
                // Add '.' to query starting from the xpath node, if not already there.
                // TODO GET RID OF THIS, BUT UPDATE THE CALLING POLICIES.
                if (xPathToParent[0] != '.') { xPathToParent = xPathToParent.Insert(0, "."); }

                AddNodeWithXml(selectorNode, xPathToParent, localName, innerXml);
            }
        }

        /// <summary>
        /// Adds a node to the document and also sets the InnerXml property to the given value.
        /// </summary>
        /// <param name="contextNode">document to which the node will be added</param>
        /// <param name="xpathToParent">xpath to the node to which the added node will be 
        /// a child, i.e. the parent node</param>
        /// <param name="localName">name of the added node</param>
        /// <param name="innerXml">inner xml of the added node.</param>
        /// <returns>added node</returns>
        public static XmlNode AddNodeWithXml(XmlNode doc, string xpathToParent, string localName, string innerXml)
        {
            #region "Predicates"

            // add parent node if it doesn't exist
            AddNodeIfNotExists(doc, xpathToParent);
            #endregion

            XmlNode addedNode = AddNode(doc, xpathToParent, localName);

            if (null != addedNode) { addedNode.InnerXml = innerXml; }

            return addedNode;
        }

        /// <summary>
        /// Removes all nodes with the given XPath
        /// </summary>
        /// <param name="node">context node</param>
        /// <param name="xpath">selects the nodes to remove</param>
        /// <returns>updated <paramref name="node"/></returns>
        public static XmlNode RemoveNode(XmlNode node, string xpath)
        {
            #region "Predicates"
            if (null == node)
            {
                return node;
            }
            #endregion

            foreach (XmlNode n in node.SelectNodes(xpath)) { n.ParentNode.RemoveChild(n); }

            return node;
        }

        /// <summary>
        /// Returns <paramref name="doc"/> with all of the empty elements removed.
        /// </summary>
        /// <param name="contextNode">XmlDocument object instance</param>
        /// <returns>updated document</returns>
        public static XmlDocument RemoveEmptyElements(XmlDocument doc)
        {
            #region "Predicates"
            CheckDocument(doc);
            #endregion

            // This finds all of the empty ELEMENTS, not the empty nodes excluding those that might
            // be empty but still contain valid attributes

            XmlNodeList emptyNodes = doc.SelectNodes("//*[not(node()) and not(@*)]");

            while (emptyNodes.Count > 0)
            {
                foreach (XmlNode node in emptyNodes)
                {
                    node.CreateNavigator().DeleteSelf();
                }
                emptyNodes = doc.SelectNodes("//*[not(node()) and not(@*)]");
            }
            return doc;
        }

        public static XmlNode LocateNode(XmlNode node, string xpath)
        {
            XmlNode nodeFound = null;  // returned

            // Return the node given if there is not an XPath specified?.
            if (string.IsNullOrEmpty(xpath)) { nodeFound = node; }

            // Return the first node found with the given xpath.
            else { nodeFound = node.SelectSingleNode(xpath); }

            return nodeFound;
        }

        /// <summary>
        /// Returns the first node found with <paramref name="xpath"/>given that also has the same value.
        /// </summary>
        /// <param name="node">context node</param>
        /// <param name="xpath">xpath</param>
        /// <param name="value">value to check</param>
        /// <returns>node found with the same value, or null</returns>
        public static XmlNode LocateNode(XmlNode node, string xpath, string value)
        {
            XmlNode nodeFound = null;  // returned

            #region "Predicates"
            // Return the node given if there is not an XPath specified?.
            if (string.IsNullOrEmpty(xpath)) { return node; }
            #endregion

            foreach (XmlNode n in node.SelectNodes(xpath))
            {
                string nValue = GetValue(n, ".");
                if (nValue == value)
                {
                    nodeFound = n;
                    break;
                }

            }
            return nodeFound;
        }

        public static XmlDocument GetFormattedXmlDocument(StringBuilder sb, Encoding enc)
        {
            // create XmlDocument from an XML string
            XmlDocument doc = new XmlDocument();   //returned

            using (StringWriter sw = new StringWriter())
            {
                //contextNode.LoadXml(xmlString); // xmlString is a string of XML formatted data
                doc.LoadXml(sb.ToString()); // xmlString is a string of XML formatted data

                // generate a formatted, indented XML string from an XmlDocument
                //StringWriter sw = new StringWriter();
                XmlTextWriter xtw = new XmlTextWriter(sw);
                xtw.Formatting = Formatting.Indented;

                doc.WriteTo(xtw);
                xtw.Flush();
                xtw.Close();

            }
            return doc;
        }

        /// <summary>
        /// Automatically creates an XmlNamespaceManager object for the given document
        /// </summary>
        /// <param name="contextNode"></param>
        /// <returns></returns>
        private static XmlNamespaceManager CreateNamespaceManager(XmlDocument doc)
        {
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
            foreach (XmlAttribute attr in doc.SelectSingleNode("/*").Attributes)
            {
                if (attr.Prefix == "xmlns") { nsmgr.AddNamespace(attr.LocalName, attr.Value); }
            }
            return nsmgr;
        }

        /// <summary>
        /// Sets the value of a node
        /// </summary>
        /// <param name="node"></param>
        /// <param name="xpath">selects the desired node whose value we want to set</param>
        /// <param name="val">value to set</param>
        /// <param name="addIfNotExists">if true, then add the node that <paramref name="xpath"/> selects 
        /// if it does not already exist exist; otherwise, do not add it and hence no value will be set</param>
        /// <returns>the node whose value was set</returns>
        public static XmlNode SetValue(XmlNode node, string xpath, object val, bool addIfNotExists)
        {

            XmlNode n = LocateNode(node, xpath);

            // node doesn't exist, so create it
            if (null == n && addIfNotExists) { n = AddNodeIfNotExists(node, xpath); }

            n.InnerText = val.ToString();

            return n;
        }

        /// <summary>
        /// Returns the first node found
        /// </summary>
        /// <param name="xmlNode">context node</param>
        /// <param name="xPath">XPath xpath</param>
        /// <returns></returns>
        public static XmlNode SelectFirstNode(XmlNode xmlNode, string xPath)
        {
            XmlNode parent = xmlNode;
            XmlNode node = parent;

            if (null != parent)
            {
                if (!string.IsNullOrEmpty(xPath))
                {
                    // Return the first node that the XPath points to.
                    node = parent.SelectSingleNode(xPath);
                }
            }

            return node;
        }

        /// <summary>
        /// Returns the value found within <paramref name="node"/> 
        /// with <paramref name="xpath"/>
        /// </summary>
        /// <param name="node">context node</param>
        /// <param name="xpath">XPath to use to obtain a value</param>
        /// <returns>value found or string.Empty</returns>
        public static string GetValue(XmlNode node, string xpath)
        {
            string value = string.Empty;

            if (null != node)
            {
                XmlNode target = node.SelectSingleNode(xpath);

                if (null != target)
                {
                    switch (target.NodeType)
                    {
                        case XmlNodeType.Attribute:
                            value = target.Value;
                            break;
                        case XmlNodeType.CDATA:
                            break;
                        case XmlNodeType.Comment:
                            break;
                        case XmlNodeType.Document:
                            break;
                        case XmlNodeType.DocumentFragment:
                            break;
                        case XmlNodeType.DocumentType:
                            break;
                        case XmlNodeType.Element:
                            value = target.InnerText;
                            break;
                        //case XmlNodeType.EndElement:
                        //    break;
                        //case XmlNodeType.EndEntity:
                        //    break;
                        //case XmlNodeType.Entity:
                        //    break;
                        //case XmlNodeType.EntityReference:
                        //    break;
                        //case XmlNodeType.None:
                        //    break;
                        //case XmlNodeType.Notation:
                        //    break;
                        //case XmlNodeType.ProcessingInstruction:
                        //    break;
                        //case XmlNodeType.SignificantWhitespace:
                        //    break;
                        //case XmlNodeType.Text:
                        //    break;
                        //case XmlNodeType.Whitespace:
                        //    break;
                        //case XmlNodeType.XmlDeclaration:
                        //    break;
                        default:
                            break;
                    }
                }
            }
            return value;
        }

        /// <summary>
        /// Returns a BizTalk Document Type 
        /// </summary>
        /// <param name="">Xmldocument</param>
        /// <returns>string representing Namespace#Root</returns>
        public static string GetBizTalkDocumentType(XmlDocument xdoc)
        {
            return string.Format("{0}#{1}", xdoc.DocumentElement.NamespaceURI, xdoc.DocumentElement.LocalName);
        }
    }
}
